/*

This file is part of roomyak.

Copyright (C) 2008 Pixecur Technologies Incorporated (Esotera).
Visit Esotera at http://www.esoteras3.com
 
roomyak is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
 
roomyak is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with roomyak; see the file COPYING.  If not, write to 
Pixecur Technologies, 213-31 Peet Street, St. John's, NL, A1B 3W8. 
 
Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.
 
*/

package com.datasentinel.util.data;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.Random;
import java.util.logging.Logger;

import javax.crypto.Cipher;

import com.datasentinel.util.io.StreamUtils;


/**
 * 
 * @author Areg Beketovski
 *
 */
public class ByteArray extends ReadOnlyByteArray implements Serializable
{
	private static final long serialVersionUID = 1L;

	protected static final Logger logger = Logger.getLogger(ByteArray.class.getName());

	transient protected OutputStream outputStream;

	public ByteArray(int size)
	{
		super(size);
	}

	protected ByteArray(byte[] data, boolean copy)
	{
		super(data, copy);
	}

	public void set(ByteArray byteArray) throws ArrayIndexOutOfBoundsException
	{
		set(byteArray, byteArray.length());
	}

	public void set(ByteArray byteArray, int length) throws ArrayIndexOutOfBoundsException
	{
		set(0, byteArray.data, 0, length);
	}

	public void set(int index, byte value) throws ArrayIndexOutOfBoundsException
	{
		if (this.data == null || index < 0 || index >= length())
			throw new ArrayIndexOutOfBoundsException("Empty array");
		//		logger.warning(this + ".set("  + index + ")");
		this.data[index] = value;
	}

	public void set(ReadOnlyByteArray byteArray, int length) throws ArrayIndexOutOfBoundsException
	{
		set(byteArray, 0, length);
	}

	public void set(ReadOnlyByteArray byteArray, int srcOffset, int length) throws ArrayIndexOutOfBoundsException
	{
		set(0, byteArray, srcOffset, length);
	}


	public void set(int offset, ReadOnlyByteArray byteArray, int srcOffset, int length)
			throws ArrayIndexOutOfBoundsException
	{
		if (this.data == null || offset < 0 || length < 0 || (offset + (length - srcOffset)) > length())
			throw new ArrayIndexOutOfBoundsException("Empty array");

		//		logger.warning(this + ".set("  + offset + ", " + length + ") of " + byteArray);
		System.arraycopy(byteArray.data, srcOffset, data, offset, length);
	}

	public void set(byte b[], int srcOffset, int length) throws ArrayIndexOutOfBoundsException
	{
		set(0, b, srcOffset, length);
	}

	public void set(int offset, byte b[], int srcOffset, int length) throws ArrayIndexOutOfBoundsException
	{
		if (this.data == null || offset < 0 || length < 0 || (offset + (length - srcOffset)) > length())
			throw new ArrayIndexOutOfBoundsException("Empty array");

		//		logger.warning(this + ".set("  + offset + ", " + length + ")");
		System.arraycopy(b, srcOffset, data, offset, length);
	}

	public void shift(int offset, int length, int shiftAmount) throws ArrayIndexOutOfBoundsException
	{
		if (shiftAmount < 0)
		{
			for (int i = 0; i < length; ++i)
			{
				data[i + offset + shiftAmount] = data[i + offset];
				data[i + offset] = 0;
			}
		}
		else
		{
			for (int i = length - 1; i >= 0; --i)
			{
				data[i + offset + shiftAmount] = data[i + offset];
				data[i + offset] = 0;
			}
		}
	}


	public void resetOutputStream()
	{
		getOutputStream();
	}

	public OutputStream getOutputStream()
	{
		return getOutputStream(0);
	}

	public OutputStream getOutputStream(int offset)
	{
		if (outputStream == null)
		{
			createOutputStream(offset);
		}

		if (this.outputStream instanceof FixedSizeByteArrayOutputStream)
		{
			((FixedSizeByteArrayOutputStream)this.outputStream).reset(offset);
		}
		else
		{
			createOutputStream(offset);
		}

		return outputStream;
	}


	protected void resetOutputStream(int offset)
	{

	}

	protected void createOutputStream(int offset)
	{
		this.outputStream = new FixedSizeByteArrayOutputStream(this.data, offset, this.data.length - offset);
	}

	public void reset()
	{
		if (this.data != null)
		{
			Arrays.fill(this.data, (byte)0);
			if (this.outputStream != null && this.outputStream instanceof ByteArrayOutputStream)
				((ByteArrayOutputStream)this.outputStream).reset();
		}
	}

	public void readByteArray(InputStream is) throws IOException
	{
		StreamUtils.readByteArray(is, data);
	}

	public void readFromInputStream(InputStream is) throws IOException
	{
		is.read(data);
	}

	public void readFromInputStream(InputStream is, int offset, int length) throws IOException
	{
		is.read(data, offset, length);
	}


	/*
	 *	Number to/from Byte manipulation methods 
	 */

	public void writeShort(int offset, short value)
	{
		writeShort(data, offset, value);
	}

	public static void writeShort(byte[] data, int offset, short value)
	{
		data[offset] = (byte)((value >>> 8) & 0xFF);
		data[offset + 1] = (byte)((value >>> 0) & 0xFF);
	}

	public void writeInt(int offset, int value)
	{
		writeInt(data, offset, value);
	}

	public static void writeInt(byte[] data, int offset, int value)
	{
		data[offset] = (byte)((value >>> 24) & 0xFF);
		data[offset + 1] = (byte)((value >>> 16) & 0xFF);
		data[offset + 2] = (byte)((value >>> 8) & 0xFF);
		data[offset + 3] = (byte)((value >>> 0) & 0xFF);
	}

	public void writeLong(int offset, long value)
	{
		writeLong(data, offset, value);
	}

	public static void writeLong(byte[] data, int offset, long value)
	{
		data[offset] = (byte)((value >>> 56) & 0xFF);
		data[offset + 1] = (byte)((value >>> 48) & 0xFF);
		data[offset + 2] = (byte)((value >>> 40) & 0xFF);
		data[offset + 3] = (byte)((value >>> 32) & 0xFF);
		data[offset + 4] = (byte)((value >>> 24) & 0xFF);
		data[offset + 5] = (byte)((value >>> 16) & 0xFF);
		data[offset + 6] = (byte)((value >>> 8) & 0xFF);
		data[offset + 7] = (byte)((value >>> 0) & 0xFF);
	}

	public void writeAsciiString(String value)
	{
		writeAsciiString(data, value);
	}

	public static void writeAsciiString(byte[] data, String value)
	{
		int length = value.length();
		for (int i = 0; i < length; ++i)
		{
			data[i] = (byte)(value.charAt(i) & 0x00FF);
		}

		if (data.length > length)
		{
			data[length] = 0;
		}
	}


	public int encryptPart(Cipher cipher, Key key, AlgorithmParameterSpec algorithmParameterSpec, int offset, int length)
			throws IOException
	{
		try
		{
			cipher.init(Cipher.ENCRYPT_MODE, key, algorithmParameterSpec);
			int encryptedLength = cipher.doFinal(this.data, offset, length, this.data, offset);
			return encryptedLength;
		}
		catch (Exception e)
		{
			throw new IOException(e);
		}
	}


	public int decryptPart(Cipher cipher, Key key, AlgorithmParameterSpec algorithmParameterSpec, int offset,
			int encryptedLength) throws IOException
	{
		try
		{
			cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameterSpec);
			int decryptedLength = cipher.doFinal(this.data, offset, encryptedLength, this.data, offset);
			return decryptedLength;
		}
		catch (Exception e)
		{
			throw new IOException(e);
		}
	}

	public void setRandomValue(Random random)
	{
		random.nextBytes(data);
	}
}
